import json
import time

from .u_datetime import UDateTime
from datetime import datetime

UNREACHABLE_TIME = 4102415999.0
LEN_MEANS_ALL = (60, 60, 24, 28, 12, 7)
DAYS = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)


class CroniterBadTypeError(TypeError):
    """."""


class CroniterBadCronError(SyntaxError):
    """ Syntax, unknown value, or range error within a cron expression """
    pass


class UCroniter(object):
    pass


class UTinyCroniter(UCroniter):
    def __init__(self, timer_type: str = "interval", specified_num: int = 0, specified_time: str = "00:00:00",
                 interval: int = 60, start_time: float = None):
        """
        微型定时器
        :param timer_type: 指定时间类型，支持指定每月/周某一天的某一时间，支持指定每天的某一时间，支持指定每小时的某一时间，支持秒级时间周期
                            取值范围：[monthly, weekly, daily, hourly, interval]
        :param specified_num: timer_type为monthly，weekly, daily时有效，monthly支持取值1到28, weekly支持取值1到7.
                            daily支持0到23
        :param specified_time: 指定的时间， timer_type为hourly时，小时的值不生效
        :param interval 秒级时间间隔，在timer_type为interval时有效
        :param start_time 计算下一次的指定开始时间，为None则取当前时间， float时间戳
        :return: re_date: diff_day天前的日期
        """
        # print(timer_type, specified_num, specified_time, interval, UDateTime.timestamp_2_str(start_time))
        if timer_type not in ["monthly", "weekly", "daily", "hourly", "interval"]:
            raise CroniterBadTypeError("not support type, just support:[monthly, weekly, daily, hourly, interval]")

        if specified_num and specified_num < 0:
            raise CroniterBadCronError("specify a certain num must > 0")

        if timer_type == "monthly" and (not specified_num or specified_num > 28):
            raise CroniterBadCronError("monthly type must specify a certain num in range 1-28")

        if timer_type == "weekly" and (not specified_num or specified_num > 7):
            raise CroniterBadCronError("weekly type must specify a certain num in range 1-7")

        self.start_time = start_time or time.time()
        self.timer_type = timer_type
        self.specified_num = specified_num
        self.specified_time = specified_time
        self.interval = interval
        self.specified_hour, self.specified_minute, self.specified_second = specified_time.split(":")
        self.next_time: dict = self.gen_next()
        self.trigger_time = 0

    def gen_next(self):
        if self.timer_type == "interval":
            res_time = self.start_time + self.interval
        elif self.timer_type == "hourly":
            res_time = self._hourly_gen_next()
        elif self.timer_type == "daily":
            res_time = self._daily_gen_next()
        elif self.timer_type == "weekly":
            res_time = self._weekly_gen_next()
        else:
            res_time = self._monthly_gen_next()

        self.next_time = {"str": UDateTime.timestamp_2_str(res_time),
                          "unix": UDateTime.timestamp_2_unix_ms_time(res_time),
                          "float": res_time}
        return self.next_time

    def _hourly_gen_next(self):
        str_start_time = UDateTime.timestamp_2_str(self.start_time)
        res_time_str = str_start_time[:-5] + self.specified_minute + ":" + self.specified_second
        res_time = UDateTime.str_2_timestamp(res_time_str)
        if res_time <= self.start_time:
            res_time = res_time + 3600  # next hour
        # print(f"gen next time: {UDateTime.timestamp_2_str(res_time)}")
        return res_time

    def _daily_gen_next(self):
        # 启动时间已经错过，则将下次启动时间调整为当前时间之后的启动时间
        curr_time = UDateTime.get_cur_time('int')
        if self.start_time < curr_time:
            str_curr_time = UDateTime.timestamp_2_str(curr_time)
            res_time_str = str_curr_time[:-8] + self.specified_time
            res_time = UDateTime.str_2_timestamp(res_time_str)
            res_time = res_time + 3600 * 24  # next day
            return res_time

        str_start_time = UDateTime.timestamp_2_str(self.start_time)
        res_time_str = str_start_time[:-8] + self.specified_time
        res_time = UDateTime.str_2_timestamp(res_time_str)
        if res_time < self.start_time:
            res_time = res_time + 3600 * 24  # next day

        return res_time

    def _weekly_gen_next(self):
        str_start_time = UDateTime.timestamp_2_str(self.start_time)
        res_time_str = str_start_time[:-8] + self.specified_time
        start_time_day = datetime.fromtimestamp(self.start_time).weekday()
        diff_day = self.specified_num - 1 - start_time_day  # -1 is for transfer 1-7 to 0-6
        res_time = UDateTime.str_2_timestamp(res_time_str) + diff_day * 24 * 3600
        if res_time <= self.start_time:
            res_time = res_time + 3600 * 24 * 7  # next week
        return res_time

    def _monthly_gen_next(self):
        str_start_time = UDateTime.timestamp_2_str(self.start_time)
        res_time_str = str_start_time[:-8] + self.specified_time
        start_time_day = datetime.fromtimestamp(self.start_time).day
        diff_day = self.specified_num - start_time_day
        res_time = UDateTime.str_2_timestamp(res_time_str) + diff_day * 24 * 3600
        if res_time <= self.start_time:
            # next month
            dt: datetime = datetime.fromtimestamp(res_time)
            diff_day = DAYS[dt.month]
            res_time = res_time + 3600 * 24 * diff_day  # next week
        return res_time

    def get_next(self, res_type: str = "float"):
        """
        获取下次定时器到期时间
        :param res_type: 结果类型，支持三种取值：str，float， unix，分别表示字符串时间，秒级float时间戳，unix毫秒级int时间戳
        :return:
        """
        return self.next_time[res_type]

    def is_expired(self, time_arg=None) -> bool:
        """
        查看下次定时器是否到期，时间参数如果为None则判断当前时间是否到定时器到期时间
        :param time_arg: 
        :return: 
        """
        if time_arg is None:
            time_arg = time.time()
        if isinstance(time_arg, float):
            return time_arg >= self.next_time["float"]
        elif isinstance(time_arg, int):
            return time_arg >= self.next_time["unix"]
        elif isinstance(time_arg, str):
            return UDateTime.str_2_timestamp(time_arg) >= self.next_time["float"]

    def expired_reset_or_nothing(self, time_arg=None) -> bool:
        """
        返回定时器是否到期，如果到期则重置定时器到下一周期，否则不做任何处理
        :param time_arg: 
        :return: 
        """
        if self.is_expired(time_arg):
            self.trigger_time = self.start_time
            self.start_time = self.next_time["float"]
            self.gen_next()
            return True
        else:
            return False


class USimplyCroniter(UCroniter):

    def __init__(self, timer_type: str = "interval", expr_format: str = None, specified_time: str = "00:00:00",
                 interval: int = 60, start_time: float = None):
        """
        微型定时器
        :param timer_type: 指定时间类型，支持指定每月/周某一天的某一时间，支持指定每天的某一时间，支持指定每小时的某一时间，支持秒级时间周期
                            取值范围：[monthly, weekly, daily, hourly, interval]
        :param expr_format: 简易定时器表达式，字符串类型，支持纯数字，数值范围和以逗号分割的整数
                            timer_type为monthly，weekly, daily时有效，monthly支持取值1到28, weekly支持取值1到7.
                            daily支持0到23
        :param specified_time: 指定的时间， timer_type为hourly时，小时的值不生效
        :param interval 秒级时间间隔，在timer_type为interval时有效
        :param start_time 计算下一次的指定开始时间，为None则取当前时间， float时间戳
        :return: re_date: diff_day天前的日期
        """
        self.timer_type = timer_type
        self.expr_format = expr_format if isinstance(expr_format, str) else str(expr_format)
        self.specified_time = specified_time
        self.interval = interval
        self.start_time = start_time
        self.trigger_time = 0
        self.cron_args = self._parse_cron_args()
        self.cron_dict = {json.dumps(kw): UTinyCroniter(**kw) for kw in self.cron_args}
        self.next_cron = None
        for k, _cron in self.cron_dict.items():
            _cron.get_next()
            if len(self.cron_dict) == 1:
                self.next_cron = _cron

    def _parse_cron_args(self):
        num_list = list()
        time_list = list()
        if self.timer_type == "interval":
            num_list.append(0)
            time_list.append(self.specified_time)
            return self._build_tiny_args(num_list, time_list)

        if self.expr_format.isdigit():
            num_list.append(int(self.expr_format))
            time_list.append(self.specified_time)
            return self._build_tiny_args(num_list, time_list)

        _trans_list = self._parse_expr()

        if not _trans_list:
            raise CroniterBadCronError

        if self.timer_type == "daily":
            num_list.append(0)
            for i in _trans_list:
                time_list.append(str(i).zfill(2) + self.specified_time[2:])
            return self._build_tiny_args(num_list, time_list)
        elif self.timer_type == "hourly":
            num_list.append(0)
            for i in _trans_list:
                time_list.append(self.specified_time[:3] + str(i).zfill(2) + self.specified_time[5:])
            return self._build_tiny_args(num_list, time_list)
        else:
            time_list.append(self.specified_time)
            num_list = _trans_list
            return self._build_tiny_args(num_list, time_list)

    def _parse_expr(self):
        _trans_list = list()
        _expr_list = self.expr_format.split("-")
        if len(_expr_list) == 2 and self._is_str_digit_valid(_expr_list):
            _max = int(_expr_list[1])
            _min = int(_expr_list[0])
            for i in range(_max - _min + 1):
                _trans_list.append(i + _min)

        _expr_list = self.expr_format.split(",")
        if len(_expr_list) > 1 and self._is_str_digit_valid(_expr_list):
            _trans_list = [int(i) for i in _expr_list]

        return _trans_list

    def _is_str_digit_valid(self, digit_str_list: list):
        try:
            if self.timer_type == "monthly":
                _max = 28
                _min = 1
            elif self.timer_type == "weekly":
                _max = 7
                _min = 1
            elif self.timer_type == "hourly":
                _max = 59
                _min = 0
            else:
                _max = 23
                _min = 0

            res = True
            for i in digit_str_list:
                res = res and _min <= int(i) <= _max

            return res
        except ValueError:
            return False

    def _build_tiny_args(self, num_list, time_list):
        res = list()
        for num in num_list:
            for _time in time_list:
                res.append({
                    "timer_type": self.timer_type,
                    "specified_num": num,
                    "specified_time": _time,
                    "interval": self.interval,
                    "start_time": self.start_time
                })
        return res

    def get_next(self, res_type: str = "float"):
        """
        获取下次定时器到期时间
        :param res_type: 结果类型，支持三种取值：str，float， unix，分别表示字符串时间，秒级float时间戳，unix毫秒级int时间戳
        :return:
        """
        if len(self.cron_dict) == 1:
            return self.next_cron.get_next(res_type)

        _next_time = UNREACHABLE_TIME
        cron = None
        for k, _cron in self.cron_dict.items():
            _time = _cron.get_next()
            if _time < _next_time:
                _next_time = _time
                cron = _cron
        self.next_cron = cron
        return cron.get_next(res_type)

    def is_expired(self, time_arg=None) -> bool:
        """
        查看下次定时器是否到期，时间参数如果为None则判断当前时间是否到定时器到期时间
        :param time_arg:
        :return:
        """
        if time_arg is None:
            time_arg = time.time()
        if not isinstance(self.next_cron, UTinyCroniter):
            self.get_next()

        if isinstance(time_arg, float):
            return time_arg >= self.next_cron.next_time["float"]
        elif isinstance(time_arg, int):
            return time_arg >= self.next_cron.next_time["unix"]
        elif isinstance(time_arg, str):
            return UDateTime.str_2_timestamp(time_arg) >= self.next_cron.next_time["float"]

    def expired_reset_or_nothing(self, time_arg=None) -> bool:
        """
        返回定时器是否到期，如果到期则重置定时器到下一周期，否则不做任何处理
        :param time_arg:
        :return:
        """
        res = False
        for _, _cron in self.cron_dict.items():
            res = _cron.expired_reset_or_nothing(time_arg) or res
            if self.trigger_time < _cron.trigger_time:
                self.trigger_time = _cron.trigger_time

        if res:
            self.get_next()
            return True
        else:
            return False

    def _reset_start_time(self, start_time: float):
        for _, _cron in self.cron_dict.items():
            _cron.start_time = start_time


if __name__ == "__main__":
    start = "2024-07-16 20:15:00"

    a = {
        "timer_type": "hourly",
        "expr_format": "0,15,30,45",
        "specified_time": "00:00:00",
        "interval": 900,
        "start_time": UDateTime.str_2_timestamp(start)
    }
    b = USimplyCroniter(**a)
    trigger_time = b.get_next("str")
    print("start:, now trigger time: ", trigger_time)
    print(b.expired_reset_or_nothing(trigger_time))
    print("after 1 period, now trigger time: ", b.get_next("str"))
    trigger_time = b.get_next("str")
    print(trigger_time)
    print(b.expired_reset_or_nothing(trigger_time))
    print("after 2 period, now trigger time: ", b.get_next("str"))
    trigger_time = b.get_next("str")
    print(trigger_time)
    print(b.expired_reset_or_nothing(trigger_time))
    print("after 3 period, now trigger time: ", b.get_next("str"))
    trigger_time = b.get_next("str")
    print(trigger_time)
    print(b.expired_reset_or_nothing(trigger_time))
    print("after 4 period, now trigger time: ", b.get_next("str"))
    for _, _v in b.cron_dict.items():
        print(_v.get_next("str"))
